package org.swift.mashup.engine.deployment;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMFactory;
import org.apache.axiom.om.OMNamespace;
import org.apache.axiom.om.impl.llom.OMDocumentImpl;
import org.apache.axiom.om.impl.llom.OMElementImpl;
import org.apache.axiom.soap.SOAPEnvelope;
import org.apache.axis2.AxisFault;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.engine.AxisEngine;
import org.apache.axis2.receivers.AbstractMessageReceiver;
import org.apache.axis2.transport.http.HTTPConstants;
import org.apache.axis2.util.MessageContextBuilder;
import org.apache.log4j.Logger;
import org.swift.commons.util.StringUtils;
import org.swift.commons.xml.OMUtils;
import org.swift.commons.xml.XmlUtils;
import org.swift.commons.xml.validation.XMLValidationResult;
import org.swift.mashup.engine.MashupException;
import org.swift.mashup.engine.concurrency.agenda.ActionException;
import org.swift.mashup.engine.concurrency.execution.ExecutionOutput;
import org.swift.mashup.engine.concurrency.execution.ExecutionOutputMode;
import org.swift.mashup.engine.deployment.mcr.NoOpMessageContextReader;
import org.swift.mashup.engine.model.Mashup;
import org.w3c.dom.Document;

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

public class MashupMessageReceiver extends AbstractMessageReceiver {
	private static final String MASHUP_ROOT_TAG_NAME = "mashupResults";
	private static final int MAX_CAUSES = 10;

	private static Logger log = Logger.getLogger(MashupMessageReceiver.class);

	private Mashup mashup = null;
	private MashupResultFormatter<Map<String,OMElement>> formatter = null;

	private Map<String,String> mediaTypes = new HashMap<String,String>();
	
	private MessageContextReader reader = NoOpMessageContextReader.getInstance();

	public MashupMessageReceiver(Mashup mashup) {
		setMashup(mashup);
		this.formatter = new OMResultFormatter();
	}
	
	public MashupMessageReceiver (Mashup mashup, MashupResultFormatter<Map<String,OMElement>> formatter){
		setMashup(mashup);
		this.formatter = formatter;
	}

    protected Mashup getMashup() {
        return mashup;
    }
    private void setMashup(Mashup mashup){
        this.mashup = mashup;
    }

    /**
	 * TODO: Document me.
	 * 
	 * @param messageContext
	 * @param parameters
	 */
	@SuppressWarnings("rawtypes")
	protected void printRequestInformation(MessageContext messageContext,
			Map parameters) {
		String message = "";
		if (messageContext.isSOAP11())
			message += "SOAP 1.1 request received.";
		else if (messageContext.isDoingREST())
			message = String.format(
					"REST request with HTTP method [%s] received.",
					messageContext.getProperty(HTTPConstants.HTTP_METHOD));
		else
			message += "SOAP 1.2 request received.";
		log.debug(message);
		log.debug("Input parameters to mashup: " + parameters);
	}

	protected void invokeBusinessLogic(MessageContext messageContext)
			throws AxisFault {
		OMElement xmlResults;
		final String operationName = messageContext.getOperationContext().getOperationName();
		try {		
			if ("runMashup".equals(operationName)) {
				xmlResults = runMashup(messageContext);
			} else if ("runConcurrent".equals(operationName) && getMashup().isParallel()) {
				xmlResults = runConcurrent(messageContext);
			} else {
				throw new AxisFault("invalid operation");
			}
				
			SOAPEnvelope envelope = this.getSOAPFactory(messageContext)
					.getDefaultEnvelope();
			envelope.getBody().addChild(xmlResults);
			MessageContext outMsgContext = MessageContextBuilder
					.createOutMessageContext(messageContext);
			outMsgContext.getOperationContext()
					.addMessageContext(outMsgContext);
			outMsgContext.setEnvelope(envelope);
			replicateState(messageContext);
			AxisEngine.send(outMsgContext);
		} catch(MashupException mashupException) {
			log.error("MashupException occured in MashupMessageReceiver: ", mashupException);

			throw new AxisFault(new QName("http://www.swift.org", 
					String.valueOf(mashupException.getMashupExceptionCode()), 
					"swiftMashupError"), 
					mashupException.getMessage(), 
					null, 
					null, 
					createErrorDetail(mashupException));
		} catch(Throwable throwable) {
			String message = null;
			if(throwable instanceof ActionException) {
				ActionException ae = (ActionException) throwable;
				StringBuilder buf = new StringBuilder();
				if(StringUtils.isNotBlank(ae.getActionName())){
					buf.append("Failed on node \"").append(ae.getActionName()).append("\": ");
				}
				buf.append(ae.getMessage());
				message = buf.toString();
			}
			else {
				message = "Throwable occured in MashupMessageReceiver: ";
			}
			Throwable rootCause = getUltimateCause(throwable);
			log.error(message, rootCause);

			throw new AxisFault(new QName("http://www.swift.org", 
					String.valueOf(MashupException.OTHER_EXCEPTION), 
					"swiftMashupError"), 
					throwable.getMessage(), 
					null, 
					null, 
					createErrorDetail(rootCause));
		}
	}

	private static Throwable getUltimateCause(Throwable th) {
		Throwable previous = null;
		Throwable current = th;
		int num = 0;
		// very simple method to get cause, doesn't try to check alternate
		// known methods for retrieving nested exepctions from exotic APIs (as does
		// commons-lang's ExceptionUtils). It also doesn't check for loops (to save
		// heap space and processing time), however it won't go more than MAX_CAUSES deep
		while(current != null && num < MAX_CAUSES) {
			previous = current;
			current = previous.getCause();
			num++;
		}
		return current == null ? previous : current;
	}

	@SuppressWarnings("rawtypes")
	private OMElement runConcurrent(MessageContext messageContext) throws XMLStreamException, NumberFormatException, MashupException {
		Map params = this.reader.createParameterMap(messageContext);
		String result = getMashup().runConcurrent(
				(String)params.get("processid"), 
				(String)params.get("sessionid"), 
				(String)params.get("workingDirectory"), 
				Integer.parseInt((String)params.get("startIndex")),
				Integer.parseInt((String)params.get("endIndex")));
		return OMUtils.stringToOMElement("<result> " + result + "</result>");		
	}

	@SuppressWarnings("rawtypes")
	private OMElement runMashup(MessageContext messageContext) throws Exception {
		validateAgainstSchema(messageContext);
		Map parameters = this.reader.createParameterMap(messageContext);
		if (log.isDebugEnabled()) {
			printRequestInformation(messageContext, parameters);
		}
		ExecutionOutput output = getMashup().run(parameters);
		if (!output.hasErrors()){
			
			return createResponse(output, (OMNamespace) null, messageContext);
		} else {
			ActionException ae = output.getFirstException();
			throw ae;
		}
	}
	
	/**
	 * Validates the incoming service XML against either a Pattern XML (Rule) or
	 * against an XSD structure.  
	 * 
	 * @param  messageContext context from the service call.
	 * @throws Exception if the validation rules fail for any reason
 	 */
	private void validateAgainstSchema(MessageContext messageContext) throws Exception {
		HttpServletRequest 	request 			= (HttpServletRequest)messageContext.getProperty("transport.http.servletRequest");
		Document 			postBody			= null;
		Document			validationDom		= null;
		ValidationProcess	validationProcess	= null;
		
		if( !"POST".equals(request.getMethod()) && !"PUT".equals(request.getMethod()) ) {
			return;
		}
		Mashup mash = getMashup();
		if( mash.getValidationRules() != null ) {
			//Capture the POST/PUT results and convert it to a Document
			postBody = XmlUtils.toDocument(
					OMUtils.toDOM(messageContext.getEnvelope().getBody().getFirstElement()));

			//Build a Matches and Required map from the validation pattern XML
			validationDom		= mash.getValidationRules();
			validationProcess	= new MatchesValidationProcess();

			//Validate the Matches and Required rules.
			if ( !validationProcess.validateRule(postBody, validationDom) ) {
				throw new Exception("Mashup Validation Rules Failed");
			}
		}
		
		if(mash.getSchema() != null) {
			if ( postBody != null ) {
				postBody = XmlUtils.toDocument(OMUtils.toDOM(messageContext.getEnvelope().getBody().getFirstElement()));
			}
			
			XMLValidationResult result = XmlUtils.validate(postBody, mash.getSchema());
			if(!result.isValid()) {
				throw new Exception(result.getMessage(), result.getCause());
			}
		}
	}
		
	/**
	 * 
	 * @param output
	 * @param namespace
     * @param messageContext
	 * @return
	 * @throws org.swift.mashup.engine.deployment.FormatterException
	 * @throws Exception
	 */
	protected OMElement createResponse(ExecutionOutput output, OMNamespace namespace,MessageContext messageContext)
            throws FormatterException  {
		// Need to see if more than one result was published. If this is the
		// case, then the document fragments need merged into one result
		// document
        Map<String, Collection<Object>> mashupResults = output.getResults();
		Map<String,OMElement> formattedEls = this.formatter.formatResults(mashupResults);
        Map<String,OMElement> bodyEls = new HashMap<String, OMElement>();
        HttpServletResponse response = (HttpServletResponse)messageContext.getProperty(HTTPConstants.MC_HTTP_SERVLETRESPONSE);

        for ( Map.Entry<String,OMElement> entry : formattedEls.entrySet() ) {
            OMElement element = entry.getValue();
            if ( ExecutionOutputMode.isOutputModeNamespace( element.getQName().getNamespaceURI() ) ) {
                applyOMElementToResponse(element, response);
            }
            else {
                bodyEls.put(entry.getKey(), element);
            }
        }

		if (bodyEls.size() != 1) {
			OMFactory factory = OMAbstractFactory.getOMFactory();
			OMDocumentImpl resultDocument = new OMDocumentImpl(factory);
			resultDocument.setCharsetEncoding("UTF-16");
			OMElement rootEl = new OMElementImpl(MASHUP_ROOT_TAG_NAME, namespace, factory);
			resultDocument.addChild(rootEl);
			for (OMElement element : bodyEls.values()){
				rootEl.addChild(element);
			}
			return resultDocument.getOMDocumentElement();
		} else {
            Map.Entry<String,OMElement> firstEntry = bodyEls.entrySet().iterator().next();

			if(bodyEls.size() != 0){
				setMediaType(messageContext, firstEntry.getKey());
			}

			return firstEntry.getValue();
		}
	}

    private void applyOMElementToResponse(OMElement element, HttpServletResponse response) {
        QName qname = element.getQName();
        ExecutionOutputMode mode = ExecutionOutputMode.getOutputModeForNamespace(qname.getNamespaceURI());
        String key = qname.getLocalPart();
        String value = element.getText();

        if ( ExecutionOutputMode.HEADER.equals(mode) ) {
            response.setHeader(key, value);
        }
        else if ( ExecutionOutputMode.COOKIE.equals(mode) ) {
            response.addCookie( new Cookie(key, value) );
        }
        else if ( ExecutionOutputMode.STATUSCODE.equals(mode) ) {
            response.setStatus( Integer.parseInt(value, 10) );
        }
    }

	protected void setReader(MessageContextReader reader) {
		this.reader = reader;
	}

	protected OMElement createErrorDetail(Throwable throwable) throws AxisFault {
		OMFactory factory = OMAbstractFactory.getOMFactory();
		OMDocumentImpl resultDocument = new OMDocumentImpl(factory);
		resultDocument.setCharsetEncoding("UTF-16");
		OMElement rootElement = new OMElementImpl("mashupError", (OMNamespace) null, factory);
		resultDocument.addChild(rootElement);

		// add exception message as attribute at root element
		rootElement.addAttribute("message", throwable.getMessage(), null);

		// add mashup exception code as atttribute to root element
		String code = "-1";
		if(throwable instanceof MashupException) {
			code = String.valueOf(((MashupException)throwable).getMashupExceptionCode());
		}
		rootElement.addAttribute("code", code, null);
		

		// add stacktrace as element
		StackTraceElement[] stackTraceElements = throwable.getStackTrace();
		StringBuffer buffer = null;
		if((stackTraceElements != null) && (stackTraceElements.length > 0)) {
			buffer = new StringBuffer("<stacktrace>");
			for(StackTraceElement stElement : stackTraceElements) {
				buffer.append(stElement.toString()).append('\n');
			}
			buffer.append("</stacktrace>");
			try {
				rootElement.addChild(OMUtils.stringToOMElement(buffer.toString()));
			} catch (XMLStreamException e) {
				throw new AxisFault(e.getMessage());
			}
		}

		// add cause as element
		Throwable cause = throwable.getCause();
		if(cause != null) {
			OMElement causeElement = new OMElementImpl("cause", (OMNamespace) null, factory);
			causeElement.addAttribute("message", cause.getMessage(), null);
			rootElement.addChild(causeElement);
	
			// add stacktrace to cause
			stackTraceElements = cause.getStackTrace();
			if((stackTraceElements != null) && (stackTraceElements.length > 0)) {
				buffer = new StringBuffer("<stacktrace>");
				for(StackTraceElement stElement : stackTraceElements) {
					buffer.append(stElement.toString()).append('\n');
				}
				buffer.append("</stacktrace>");
				try {
					causeElement.addChild(OMUtils.stringToOMElement(buffer.toString()));
				} catch (XMLStreamException e) {
					throw new AxisFault(e.getMessage());
				}
			}
		}

		return rootElement;
	}
	
	protected void addMediaType(String name,String value){
		this.mediaTypes.put(name, value);
	}
	
	private void setMediaType(MessageContext messageContext, String key){
		String mediaType = mediaTypes.get(key);
		if(StringUtils.isNotBlank(mediaType)){
			messageContext.setProperty("messageType", mediaType);
		}
	}
}
